#!/usr/bin/env python

'''
  MUDServ - A Multi-User Dungeon Server
  A Pygame-based Client
  
  Author: Michael Chest
  
  Copyright (c) 2009, Ecl3ctic Software
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  * Neither the name of Ecl3ctic Software nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
'''

import socket
import sys
import logging
import platform
import datetime
import simplejson
import threading
import pygame
import math
from pygame import *
from time import sleep
from clientlib import *

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s %(message)s',
                    filename='mudserv.log')

# Client version string
__version__ = '04-11-09'

consoleCommands = ['tell', 'who', 'version', 'motd', 'setname', 'wall']
nMessages = 4     # Number of lines for the message buffer
username = ''
newMap = False      # Set when we need to load up a new map

KEY_ESCAPE = 0x1B
KEY_DEL = 0x7F

imgMap = {}
fileNameMap = {}

def loadImg(imgName):
  if imgName not in imgMap:
    imgMap[imgName] = image.load(imgName);
  return imgMap[imgName]

def getFileName(num):
  if num not in fileNameMap:
    if num == -1:
      fileNameMap[-1] = "imgs/Background.png"
      return "imgs/Background.png"
    elif num == -2:
      fileNameMap[-2] = "imgs/Floor.png"
      return "imgs/Floor.png"
    result = ""
    numCopy = num
    size = 0 if num == 0 else int(math.floor(math.log(num) / math.log(2)))
    for i in range(size, -1, -1):
      result = "%s%d" % (result, numCopy / (2 ** size))
      if numCopy / (2 ** size) == 1:
        numCopy = numCopy - (2 ** size)
      size -= 1
    while len(result) % 4 != 0:
      result = "0%s" % result
    fileNameMap[num] = "imgs/%s-Wall.png" % result
  return fileNameMap[num]

def traverse(struct, point):
  y, x = point
  if y >= 0 and x >= 0 and y < len(struct) and x < len(struct[0]) and struct[y][x] == -2:
    if y != 0 and struct[y-1][x] == -1:
      struct[y-1][x] = -2
      traverse(struct, (y-1, x))
    if y != len(struct) - 1 and struct[y+1][x] == -1:
      struct[y+1][x] = -2
      traverse(struct, (y+1, x))
    if x != 0 and struct[y][x-1] == -1:
      struct[y][x-1] = -2
      traverse(struct, (y, x-1))
    if x != len(struct[y]) - 1 and struct[y][x+1] == -1:
      struct[y][x+1] = -2
      traverse(struct, (y, x+1))

def interpretBoardData(fileData, myLoc):
  dataStructure = []
  curLine = []
  for c in fileData:
    if c == ' ' or c == '+' or c == 'S':
      curLine.append(-1)
    elif c == '#':
      curLine.append(15)
    elif c == '\n':
      dataStructure.append(curLine[:-1])
      curLine = []
  #dataStructure.append(curLine[:-1])
  spawn = myLoc
  for y, line in enumerate(dataStructure):
    for x, ele in enumerate(line):
      if ele >= 0:
        t = ele
        if y != 0 and dataStructure[y-1][x] >= 0:
          t -= 8
        if y != len(dataStructure) - 1 and dataStructure[y+1][x] >= 0:
          t -= 4
        if x != 0 and dataStructure[y][x-1] >= 0:
          t -= 2
        if x != len(dataStructure[y]) - 1 and dataStructure[y][x+1] >= 0:
          t -= 1
        dataStructure[y][x] = t
      elif y == myLoc[0] and x == myLoc[1]:
        dataStructure[y][x] = -2
  traverse(dataStructure, spawn)
  return dataStructure

def drawBoard(surf, struct):
  for y, line in enumerate(struct):
    for x, ele in enumerate(line):
      surf.blit(loadImg(getFileName(struct[y][x])), (x * 36, y * 36))

def getPossibleMoves(struct, x, y):
  msg = ""
  if (x - 1 >= 0 and struct[y][x-1] == -2) or x == 0:
    msg = msg + " w"
  if (x + 1 < len(struct[y]) and struct[y][x+1] == -2) or x + 1 == len(struct[y]):
    msg = msg + " e"
  if (y + 1 < len(struct) and struct[y+1][x] == -2) or y + 1 == len(struct):
    msg = msg + " s"
  if (y - 1 >= 0 and struct[y-1][x] == -2) or y == 0:
    msg = msg + " n"
  return msg

class SyncManager(threading.Thread):
  def __init__(self, sock):
    threading.Thread.__init__(self)
    self.commandQueue = []
    self.sock = sock
    self.killed = False
    self.players = []
    self.messages = []
    self.responses = []
    
  def getPlayers(self):
    '''
      Return the last list of players we got from the server
    '''
    return self.players
  
  def getNextResponse(self):
    '''
      Get the next server response received by an enqueued command that requested
      a response, blocking if necessary
    '''
    while len(self.responses) == 0:
      sleep(0.1)
    return self.responses.pop()
    
  def enqueue(self, cmd, getResponse=False):
    '''
      Adds cmd (a command to execute) to the command queue
    '''
    self.commandQueue.insert(0, (cmd, getResponse))
    
  def kill(self):
    self.killed = True
    
  def addMessage(self, message):
    self.messages.append(message)
    
  def cmdsInQueue(self):
    return len(self.commandQueue)
  
  def wait(self, num):
    num = len(self.commandQueue) - num
    if len(self.commandQueue) == 0:
      return
    if num < 0 or num > len(self.commandQueue):
      num = 0
    
    numCom = len(self.commandQueue) - num
    pluralize = "s" if numCom != 1 else ""
    print "SyncManager waiting for %d command%s to execute" % (numCom, pluralize)
    tmp = datetime.datetime.now()
    while len(self.commandQueue) > num:
      sleep(0.0001)
    print "Wait completed in %d microseconds" % (datetime.datetime.now() - tmp).microseconds
    
  def run(self):
    while not self.killed:
      while self.commandQueue != []:
        cmd = self.commandQueue.pop()
        #print "Executing Command: " + cmd[0]
        result = eval(cmd[0])
        if cmd[1]:
          self.responses.insert(0, result)
        logging.info(cmd)
        
      result = executeCommand(sock, 'getnearbyplayers')
      if result[0] == 0:
        self.players = simplejson.loads(result[1])
        
      result = executeCommand(sock, 'getmessage')
      if result[0] == 0:
        self.messages.append(result[1])
        
      sleep(1 / 3.0)

print 'Welcome to the Ecl3ctic MUD Pygame Client v' + __version__ + '!'
  
# Setup a socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

cla = CommandLineArguments(sys.argv)

if "d" in cla.getFlags():
  debug = True

# Get the host and port information
if len(cla.getParams()) < 2:
  host = 'localhost'
else:
  host = cla.getParams()[1]

if len(cla.getParams()) > 2:
  port = int(cla.getParams()[2])
else:
  port = 5656

sys.stdout.write('Connecting to ' + str(host) + ' on port ' + str(port) + '...')
sys.stdout.flush()

# Open a socket
sock.connect((host, port))

sys.stdout.write('\n\n')

error = ''
lastBoard = ''
showConsole = False
consoleInput = ''

# Get server info
print "This server is running version " + executeCommand(sock, 'version')[1]
print

# Get the user's info
try:
  sys.stdout.write('username: ')
  sys.stdout.flush()
  username = raw_input()
  
  sys.stdout.write('password: ')
  sys.stdout.flush()
  password = raw_input()
except:
  # Clean up
  sock.send('/quit/')
  sock.close()

  # Raise the exception
  raise

status = executeCommand(sock, 'login', [username, password])
if status[0] != 0:
  print 'Failed to login: You either entered the wrong password or you do not own this username.'
else:
  nextBoardUpdate = datetime.datetime.now()
  x = int(executeCommand(sock, 'getinfo', [username, 'pos_x'])[1])
  y = int(executeCommand(sock, 'getinfo', [username, 'pos_y'])[1])
  bx, by = map(int, executeCommand(sock, 'getboarddimensions')[1].split())
  
  bx -= 1
  
  pygame.init()
  
  player = loadImg("imgs/PlayerMe.png")
  otherPlayers = loadImg("imgs/PlayerThem.png")
  floor = loadImg("imgs/Floor.png")
  
  dispInfo = display.Info()
  MAX_WIDTH, MAX_HEIGHT = dispInfo.current_w, dispInfo.current_h

  size = width, height = MAX_WIDTH - 100, MAX_HEIGHT - 100
  screen = pygame.display.set_mode(size)
  mapWidth, mapHeight = bx * 36, by * 36
  currentSurface = Surface((mapWidth, mapHeight), 0, screen)
  
  lastPlayerUpdate = datetime.datetime.now() - datetime.timedelta(seconds=10.0)
  lastPlayerPosList = {}
  
  try:
    syncManager = SyncManager(sock)
    syncManager.start()
    csLocX = (x * 36) - (width / 2)
    csLocY = (y * 36) - (height / 2)
    
    fd = executeCommand(sock, 'getasciiboard')[1]
    ds = interpretBoardData(fd, (y, x))
    drawBoard(currentSurface, ds)
    currentBoard = int(executeCommand(sock, 'getinfo', [username, 'board'])[1])
    
    # Status Flags
    killMe = didUpdate = moveN = moveS = moveE = moveW = False
    moveFlags = { K_LEFT: False, K_RIGHT: False, K_UP: False, K_DOWN: False }
    moveDirs = { K_LEFT: 'w', K_RIGHT: 'e', K_UP: 'n', K_DOWN: 's' }
    moveAmts = { K_LEFT: (-1, 0), K_RIGHT: (1, 0), K_UP: (0, -1), K_DOWN: (0, 1) }
  
    # Main loop
    while not killMe:
      pygame.time.delay(50)
      
      loadNewBoard = False
      didUpdate = False
      
      for i, j in moveFlags.items():
        if j:
          if moveDirs[i] in getPossibleMoves(ds, x, y):
            syncManager.enqueue("executeCommand(sock, \'move\', ['" + moveDirs[i] + "'])")
            didUpdate = True
            currentSurface.blit(floor, (x * 36, y * 36))
            if (x, y) in lastPlayerPosList.values():
              lastPlayerUpdate = datetime.datetime.now() - datetime.timedelta(seconds=3.0)
            x += moveAmts[i][0]
            y += moveAmts[i][1]
            if x < 0 or y < 0 or x >= bx or y >= by:
              loadNewBoard = True

      if loadNewBoard:
        tmp = datetime.datetime.now()
        #while syncManager.cmdsInQueue():
        #  pygame.time.wait(100)
        syncManager.wait(-1)
        x, y = map(int, executeCommand(sock, 'getinfo', [username, 'pos'])[1].split())
        fd = executeCommand(sock, 'getasciiboard')[1]
        ds = interpretBoardData(fd, (y, x))
        drawBoard(currentSurface, ds)
        didUpdate = True
        lastPlayerUpdate = datetime.datetime.now() - datetime.timedelta(seconds=10.0)

      if lastPlayerUpdate < datetime.datetime.now() - datetime.timedelta(seconds=3.0):
        lastPlayerUpdate = datetime.datetime.now()
        players = syncManager.getPlayers()
        for p in players:
          if p['username'] != username:
            currentSurface.blit(floor, (p['x'] * 36, p['y'] * 36))
            currentSurface.blit(otherPlayers, (p['x'] * 36, p['y'] * 36))
            lastPlayerPosList[p['username']] = ((p['x'], p['y']))
        syncManager.wait(1)
  
      if didUpdate:
        csLocX = (x * 36) - (width / 2)
        csLocY = (y * 36) - (height / 2)
  
      if csLocX < 0:
        csLocX = 0
      if csLocX > mapWidth - width:
        csLocX = mapWidth - width
      if csLocY < 0:
        csLocY = 0
      if csLocY > mapHeight - height:
        csLocY = mapHeight - height
  
      currentSurface.blit(player, (x * 36, y * 36))
      screen.blit(currentSurface, (0, 0), Rect(csLocX, csLocY, width, height))
  
      pygame.display.update()
      for event in pygame.event.get([KEYDOWN, KEYUP, QUIT]):
        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE) or (event.type == KEYUP and event.mod in (KMOD_META, KMOD_LMETA, KMOD_RMETA) and event.key == K_q) or (event.type == KEYUP and event.mod in (KMOD_ALT, KMOD_LALT, KMOD_RALT) and event.key == K_F4): # Mac: Meta-Q; Windows/Linux: Alt-F4
          # Quit pretty
          killMe = True
          break;
        if event.type == KEYDOWN and event.key in moveFlags: # and moveDirs[event.key] in getPossibleMoves(ds, x, y):
            moveFlags[event.key] = True
        elif event.type == KEYUP and event.key in moveFlags:
            moveFlags[event.key] = False
      # End For
      pygame.event.pump()
    # End While
  except Exception, er:
    # Quit pretty
    print er

    #if c == ord('q') and not showConsole:
    #  break
    #elif c == curses.KEY_UP and chr(boardArea.inch(y - 1, x) & 255) not in hardTiles:
    #  syncManager.enqueue("executeCommand(self.sock, 'move', ['n'])")
    #  y -= 1
    #  if y - padY < (stdscr_y / 2) and padY > 0:
    #    padY -= 1
    #elif c == curses.KEY_DOWN and chr(boardArea.inch(y + 1, x) & 255) not in hardTiles:
    #  syncManager.enqueue("executeCommand(self.sock, 'move', ['s'])")
    #  y += 1
    #  if y - padY > (stdscr_y / 2) and padY < 512 - stdscr_y:
    #    padY += 1
    #elif c == curses.KEY_RIGHT and chr(boardArea.inch(y, x + 1) & 255) not in hardTiles:
    #  syncManager.enqueue("executeCommand(self.sock, 'move', ['e'])")
    #  x += 1
    #  if x - padX > (stdscr_x / 2) and padX < 512 - stdscr_x:
    #    padX += 1
    #elif c == curses.KEY_LEFT and chr(boardArea.inch(y, x - 1) & 255) not in hardTiles:
    #  syncManager.enqueue("executeCommand(self.sock, 'move', ['w'])")
    #  x -= 1
    #  if x - padX < (stdscr_x / 2) and padX > 0:
    #    padX -= 1
    #elif c == ord('c') and not showConsole:
    #  showConsole = True
    #  cursorPos = 0
    #  consoleInput = ''
    #elif c == KEY_ESCAPE:
    #  showConsole = False
    #  stdscr.addstr(stdscr_y - 1, 0, ' ' * (10 + len(consoleInput)))
    #elif showConsole and (c == ord('\n') or c == curses.KEY_ENTER):
    #  showConsole = False
    #  stdscr.addstr(stdscr_y - 1, 0, ' ' * (10 + len(consoleInput)))
    #  
    #  # Parse the command
    #  if len(consoleInput) > 0 and consoleInput.split(' ')[0] in consoleCommands:
    #    command = consoleInput.split(' ')[0]
    #    
    #    args = []
    #    if len(consoleInput.split(' ')) > 1:
    #      args = consoleInput.split(' ')[1:]
    #    
    #    result = executeCommand(sock, command, args)
    #    if result[0] == 0 and len(result[1]) > 0:
    #      syncManager.addMessage(result[1])
    #    
    #elif showConsole and (c == curses.KEY_BACKSPACE or c == KEY_DEL):
    #  consoleInput = consoleInput[:-1]
    #  stdscr.addstr(stdscr_y - 1, 9 + len(consoleInput), ' ')
    #elif showConsole:
    #  try:
    #    consoleInput += chr(c)
    #  except:
    #    pass    # Ignore non-printing characters
    # 
    #if x == 0 or y == 0 or x == len(lastBoard.split('\n')[0]) - 2 or y == len(lastBoard.split('\n')) - 2:
    #  newMap = True
    
    #now = datetime.datetime.now()
    #if now > nextBoardUpdate or newMap:
    #  nextBoardUpdate = now + datetime.timedelta(seconds=30.0)
    #  try:
    #    if newMap:
    #      syncManager.addMessage('Loading map...')
    #      status = '\n'.join('\n'.join(syncManager.messages[-nMessages:]).split('\n')[-nMessages:])      # Get the last 5 lines of messages
    #      stdscr.addstr(stdscr_y - nMessages - 1, 0, status + (' ' * (stdscr_x - len(status) - 1)))    # Draw the status message
    #      stdscr.move(stdscr_y - (nMessages - len(status.split('\n'))) - 2, len(status.split('\n')[-1]))
    #      stdscr.refresh()
    #      
    #    while syncManager.cmdsInQueue() > 0:
    #      sleep(0.1)
    #    syncManager.enqueue("executeCommand(sock, 'getasciiboard')", True)
    #    result = syncManager.getNextResponse()
    #    if result[0] == 0:
    #      lastBoard = result[1]
    #      if newMap:
    #        syncManager.addMessage('Load complete')
    #    else:
    #      nextBoardUpdate = now
    #  except:
    #    error = 'Error communicating with the server!'
    #    break
    
    #if newMap:
      # Force a player update
      #syncManager.enqueue("executeCommand(sock, 'getnearbyplayers')", True)
      #result = syncManager.getNextResponse()
      #if result[0] == 0:
      #  players = simplejson.loads(result[1])
      #else:
      #  error = 'Error communicating with the server!'
      #  break
      
      # Update our coordinates
    #  syncManager.enqueue("executeCommand(sock, 'getinfo', [username, 'pos_x'])", True)
    #  result = syncManager.getNextResponse()
    #  if result[0] == 0:
    #    x = int(result[1])
    #    
    #  syncManager.enqueue("executeCommand(sock, 'getinfo', [username, 'pos_y'])", True)
    #  result = syncManager.getNextResponse()
    #  if result[0] == 0:
    #    y = int(result[1])
      
      # Adjust the map render
    #  padX = max(0, x - (stdscr_x / 2))
    #  padY = max(0, y - (stdscr_y / 2))
      
    #  newMap = False
    #else:
    #  players = syncManager.getPlayers()
      
    #board = lastBoard
    #boardArea.addstr(0, 0, board)
    
    #for player in players:
    #  posY = player['y']
    #  posX = player['x']
    #  avi = 'P'
    #  
    #  if player['username'] != username:
    #    boardArea.addch(posY, posX, ord(avi))
   # 
    #boardArea.addch(y, x, ord('@'))              # Draw the player
    #boardArea.refresh(padY,padX, 0,0, stdscr_y - nMessages - 2,stdscr_x - 1)
    
    #status = '\n'.join('\n'.join(syncManager.messages[-nMessages:]).split('\n')[-nMessages:])      # Get the last 5 lines of messages
    #stdscr.addstr(stdscr_y - nMessages - 1, 0, status + (' ' * (stdscr_x - len(status) - 1)))    # Draw the status message
    #stdscr.move(stdscr_y - (nMessages - len(status.split('\n'))) - 2, len(status.split('\n')[-1]))
    
    #if showConsole:
    #  stdscr.addstr(stdscr_y - 1, 0, 'Console: ' + consoleInput)
    #  stdscr.move(stdscr_y - 1, 9 + len(consoleInput))
    
    #stdscr.refresh()
    
    #sleep(1.0/60.0)

# Clean up
try:
  print "SyncManager exiting"
  syncManager.kill()
except:
  pass

try:
  print "Client requesting to quit from server"
  executeCommand(sock, 'quit')
except:
  pass
sock.close()

print "Main Program exiting"

if error:
  print error